iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
Software Development

從零打造邊緣運算閘道器:Raspberry Pi 與 Linux 底層軟硬整合實戰系列 第 17

Day 17 - 奪回控制權:實作 USB 狀態監聽,動態卸載本機 FAT32 (Lazy Unmount)

  • 分享至 

  • xImage
  •  

[!NOTE]
本系列文章已進行全域去敏與匿名化處理,專案代號統一為 DataGateway,內網 IP 統一為 10.0.0.x

昨天我們討論了邊緣儲存中最可怕的「雙腦分裂 (Split-Brain)」危機。只要 Linux (Raspberry Pi) 和 Windows (PC) 同時掛載同一個 FAT32 虛擬磁碟,資料結構必定會崩潰。

為了解決這個問題,我們設計了「軟體互斥鎖 (Software Mutex)」策略:

  • 未插線時:Linux 獨佔磁碟,寫入資料。
  • 插上線時:Linux 必須立刻卸載 (Unmount) 磁碟,然後才允許 Windows 接管。
  • 拔除線時:Linux 再次掛載 (Mount) 磁碟,奪回控制權。

但問題來了:在一個沒有螢幕、沒有鍵盤的無頭設備 (Headless Device) 上,Linux 怎麼知道「護理師現在把 USB 線插上去了」?

第一關:如何偵測 USB OTG 的插拔狀態?

在 Raspberry Pi 的底層架構中,當 USB OTG 埠 (Type-C) 偵測到外部有 5V 電壓或 USB Host 的連線請求時,dwc2 驅動程式會在系統日誌與 sysfs 中產生狀態改變。

在 ConfigFS 的架構下,當 USB 線插上並且成功與 PC 交握 (Enumeration) 後,UDC 的狀態會從 not attached 轉變為 configured

我們可以直接讀取 UDC 的 state 檔案來得知當前狀態:

cat /sys/class/udc/fe980000.usb/state
# 若未插線:not attached
# 若已插線並與 PC 連線:configured 或 suspended

有了這個基礎,我們就能在 Python 中寫一個無窮迴圈的監聽程式 (Monitor Thread)。

第二關:實作狀態機監聽 (Python)

這支監聽程式的職責很單純:每一秒檢查一次 UDC 狀態,並根據狀態切換執行對應的 Mount/Unmount 腳本。

import os
import time
import subprocess
import threading

class UsbMonitor:
    def __init__(self):
        # 動態獲取 UDC 名稱 (沿用 Day 13 的技巧)
        udc_dir = "/sys/class/udc"
        try:
            self.udc_name = os.listdir(udc_dir)[0]
            self.state_file = f"{udc_dir}/{self.udc_name}/state"
        except IndexError:
            self.state_file = None
            
        self.is_connected = False
        self.monitor_thread = None

    def read_usb_state(self):
        if not self.state_file or not os.path.exists(self.state_file):
            return "not attached"
        with open(self.state_file, 'r') as f:
            return f.read().strip()

    def _monitor_loop(self):
        while True:
            current_state = self.read_usb_state()
            
            # 判斷是否連線到 PC (configured 代表連線成功)
            is_now_connected = current_state in ["configured", "suspended"]
            
            if is_now_connected and not self.is_connected:
                print("[USB] 偵測到纜線插入!交出控制權...")
                self._handle_cable_plugged()
                self.is_connected = True
                
            elif not is_now_connected and self.is_connected:
                print("[USB] 偵測到纜線拔除!奪回控制權...")
                self._handle_cable_unplugged()
                self.is_connected = False
                
            time.sleep(1.0)

第三關:生死一瞬的 Lazy Unmount

這就是整套機制中最驚險的時刻。
_handle_cable_plugged() 被呼叫時,Linux 必須立刻卸載 (umount) /mnt/usb_sync

但是,如果此時 DataGateway 剛好正在寫入最新的心跳資料呢?
如果另一個 Thread 正在執行 open('/mnt/usb_sync/data.csv', 'w'),檔案被佔用 (Device is busy),標準的 umount 指令就會失敗報錯。

若卸載失敗,而 Windows 又強行接管,雙腦分裂就發生了!

為了解決這個「正在寫入時被拔插」的終極難題,我們必須祭出 Linux 的秘密武器:Lazy Unmount (umount -l)

什麼是 Lazy Unmount?

當你執行 umount -l /mnt/usb_sync 時,Linux 會做兩件事:

  1. 立即將這個掛載點從檔案系統樹 (VFS) 中拔除。 這意味著所有未來的存取都會被拒絕 (例如 Windows 端或新的 Python Thread 都無法再寫入)。
  2. 等待已開啟的檔案控制代碼 (File Descriptors) 關閉。 如果有 Thread 還在寫檔案,Linux 會讓它在背景默默寫完,等它呼叫 close() 後,才真正切斷實體 Loop Device 的聯繫。

這簡直就是為我們的邊緣設備量身打造的救命仙丹!

    def _handle_cable_plugged(self):
        # 1. 確保所有快取刷入實體檔案
        subprocess.run(['sync'])
        
        # 2. 使用 Lazy Unmount,即使有檔案正在寫入也不會報錯
        subprocess.run(['sudo', 'umount', '-l', '/mnt/usb_sync'])
        
        # 3. 釋放 Loop Device
        # 注意:如果有程序還沒寫完,losetup -d 會失敗,所以我們稍後會由排程器重試
        subprocess.run(['sudo', 'losetup', '-d', '/dev/loop0'], check=False)
        print("[USB] 磁碟已交由 Windows 接管")

    def _handle_cable_unplugged(self):
        # 1. 確保 Loop Device 乾淨
        subprocess.run(['sudo', 'losetup', '-d', '/dev/loop0'], check=False)
        
        # 2. 重新綁定並掛載
        subprocess.run(['sudo', 'losetup', '/dev/loop0', '/opt/gateway/usb_disk.img'])
        subprocess.run(['sudo', 'mount', '-t', 'vfat', '-o', 'uid=1000,gid=1000', '/dev/loop0', '/mnt/usb_sync'])
        print("[USB] 磁碟已掛載回 Linux,準備寫入新資料")

[!WARNING]
Lazy Unmount 雖然強大,但它是一種「非同步」的操作。如果你在 Lazy Unmount 尚未完全結束前,就立刻嘗試重新掛載,可能會引發 Kernel Panic。因此,在插拔瞬間的防彈跳 (Debounce) 處理極為重要。

透過 UDC 狀態監聽與 Lazy Unmount 的結合,我們終於徹底解決了雙腦分裂的問題。

但是... 你以為這樣就結束了嗎?
第二天,護理站打電話來抱怨:「為什麼我明明按了 Windows 的『安全移除硬體』,拔下設備再插上去,最新的一筆資料卻不見了?」

明天,我們將面對 Windows 作業系統最狡猾的敵人:Windows 緩存的逆襲


上一篇
Day 16 - 雙腦分裂 (Split-Brain) 危機:當 PC 與 Pi 同時寫入磁碟的毀滅性後果
下一篇
Day 18:邊緣設備的高頻同步實戰—從 Race Condition 到二補數對齊
系列文
從零打造邊緣運算閘道器:Raspberry Pi 與 Linux 底層軟硬整合實戰26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言